#!/usr/bin/env python
import sys
import os
import argparse
import json
import logging
import subprocess
import shutil
import time
import datetime
import threading
from pathlib import Path
from typing import Dict, Any
from datetime import datetime, timezone

SOFTWARE_VERSION = "101"

class ChipHelpFormatter(argparse.RawTextHelpFormatter):
    def _format_args(self, action, default_metavar):
        if action.choices:
            return ""
        return super()._format_args(action, default_metavar)

    def _add_choices(self, action):
        if action.choices and '%(choices)' not in action.help:
            return "\n可选型号:\n" + "\n".join(f"  • {chip}" for chip in action.choices)
        return ""

    def _format_action_invocation(self, action):
        if action.dest == 'chip':
            return "-c CHIPS"
        return super()._format_action_invocation(action)

# 配置管理类
class ConfigManager:
    _instance = None
    BUILD_TOOLS = ['mdk', 'iar', 'gcc']
    DOWNLOAD_TOOLS = ['j-link', 'dap-link', 'st-link']
    CONFIG_CHIPS = 'chips'
    CONFIG_COMPILERS = 'compilers'
    CONFIG_PROGRAMMERS = 'programmers'
    CONFIG_COMMON = 'common'

    def __new__(cls):
        if not cls._instance:
            cls._instance = super().__new__(cls)
            # 使用脚本所在目录来定位build_config.json
            script_dir = Path(__file__).parent
            config_path = script_dir / 'build_config.json'
            with open(config_path) as f:
                cls._instance.config = json.load(f)
        return cls._instance

    def _get_chip_config(self, chip_name: str) -> Dict[str, Any]:
        """获取合并后的芯片配置"""
        if chip_name not in self.config[self.CONFIG_CHIPS]:
            raise ValueError(f"Unsupported chip: {chip_name}")

        chip_config = self.config[self.CONFIG_CHIPS][chip_name].copy()
        common_config = self.config[self.CONFIG_COMMON].copy()

        # 合并配置，芯片特定配置优先
        merged_config = {}
        for key, value in common_config.items():
            if isinstance(value, str) and '{chip}' in value:
                merged_config[key] = value.format(chip=chip_name)
            else:
                merged_config[key] = value

        # 更新芯片特定配置
        merged_config.update(chip_config)
        return merged_config

    @property
    def chips(self) -> Dict[str, Any]:
        """返回所有芯片的合并配置"""
        return {chip: self._get_chip_config(chip) for chip in self.config[self.CONFIG_CHIPS]}

    @property
    def compilers(self) -> Dict[str, Any]:
        return self.config[self.CONFIG_COMPILERS]

    @property
    def programmers(self) -> Dict[str, Any]:
        # 添加键名转换，将带连字符的参数转为配置中的命名方式
        return {
            'j-link': self.config[self.CONFIG_PROGRAMMERS]['jlink'],
            'dap-link': self.config[self.CONFIG_PROGRAMMERS]['daplink'],
            'st-link': self.config[self.CONFIG_PROGRAMMERS]['stlink']
        }

    @classmethod
    def get_chip_list(cls) -> list:
        """将芯片列表获取整合到配置类"""
        try:
            return list(cls().config[cls.CONFIG_CHIPS].keys())
        except Exception as e:
            logging.error(f"加载芯片配置失败: {str(e)}")
            return []

    @classmethod
    def generate_usage(cls) -> str:
        """生成动态使用说明"""
        current_script_name = Path(__file__).parent.name + "/" + Path(__file__).name
        base_usage = f"""
通用命令格式：
--clean 参数用于全量编译，清除之前编译的中间文件。
--boot 参数用于编译bootloader代码。
python {current_script_name} build -c <芯片型号> -t [mdk, iar, gcc] [--boot] [--clean]
python {current_script_name} program -c <芯片型号> -p [j-link, dap-link, st-link]

J-Link专用参数：
--boot 烧录Bootloader
--app 烧录应用程序
--boot_add_app 烧录boot和app

查询支持的芯片型号：
python {current_script_name} -c ?
"""

        # 获取当前目录下的所有文件夹
        current_dir = Path(__file__).parent.parent
        existing_chips = []
        for chip in cls.get_chip_list():
            # 检查是否存在对应的目录
            if (current_dir / chip).exists():
                existing_chips.append(chip)

        if not existing_chips:
            return base_usage

        # 只为存在的芯片生成示例
        examples = []
        for chip in existing_chips:
            examples.extend([
                f"# {chip}示例",
                f"python {current_script_name} build -c {chip} -t mdk --clean",
                f"python {current_script_name} build -c {chip} -t mdk --boot --clean",
                f"python {current_script_name} program -c {chip} -p j-link --boot",
                f"python {current_script_name} program -c {chip} -p j-link --app",
                f"python {current_script_name} program -c {chip} -p j-link --boot_add_app",
                ""  # 添加空行分隔不同芯片的示例
            ])

        return base_usage + "\n示例命令：\n" + "\n".join(examples)

# 基础构建类
class BaseBuilder:
    def __init__(self, chip: str, compiler: str, clean: bool = False, boot: bool = False):
        self.config = ConfigManager()
        self.chip = chip
        self.compiler = compiler
        self.clean = clean
        self.boot = boot  # 新增boot参数
        self.validate_config()

        # 动画相关
        self.stop_animation = threading.Event()
        self.animation_thread = threading.Thread(target=self.animate)
        self.lock = threading.Lock()
        self.command_start_time = time.time()

        # 添加脚本目录属性
        self.script_dir = Path(__file__).parent.absolute()

    @staticmethod
    def calculate_crc32(file_path):
        """CRC32计算"""
        crc32_table = []
        for i in range(256):
            crc = i
            for _ in range(8):
                if crc & 1:
                    crc = (crc >> 1) ^ 0xEDB88320
                else:
                    crc >>= 1
            crc32_table.append(crc)

        with open(file_path, 'rb') as f:
            crc = 0xFFFFFFFF
            while True:
                byte = f.read(1)
                if not byte:
                    break
                crc = crc32_table[(crc ^ ord(byte)) & 0xFF] ^ (crc >> 8)
            return crc ^ 0xFFFFFFFF

    @staticmethod
    def check_exe(exe_name):
        """检查可执行文件是否存在"""
        exe_path = shutil.which(exe_name)
        if not exe_path:
            logging.error(f"找不到可执行文件: {exe_name}")
            return False
        return True

    def validate_config(self):
        if self.chip not in self.config.chips:
            raise ValueError(f"Unsupported chip: {self.chip}")
        if self.compiler not in self.config.compilers:
            raise ValueError(f"Unsupported compiler: {self.compiler}")

    def animate(self):
        """动画显示方法"""
        with self.lock:
            while not self.stop_animation.is_set():
                for char in '\\|/-':
                    if self.stop_animation.is_set():
                        break
                    elapsed_time = time.time() - self.command_start_time
                    output = f'\r{char} Building... {elapsed_time:.2f}s '
                    sys.stdout.write(output)
                    sys.stdout.flush()
                    time.sleep(0.1)
            # 清除动画行
            sys.stdout.write('\r' + ' ' * 50 + '\r')
            sys.stdout.flush()

    def start_animation(self):
        """启动动画"""
        self.command_start_time = time.time()
        self.stop_animation.clear()
        self.animation_thread = threading.Thread(target=self.animate)
        self.animation_thread.start()

    def stop_animation_thread(self):
        """停止动画"""
        self.stop_animation.set()
        if self.animation_thread.is_alive():
            self.animation_thread.join()

    def _generate_filename(self, base_name, ext):
        """生成带版本信息的文件名（仅包含年月日）"""
        timestamp = datetime.now(timezone.utc).astimezone().strftime("%Y%m%d")  # 使用本地时区时间
        return f"{base_name}_v{SOFTWARE_VERSION}_{timestamp}_0x{self._get_file_crc(ext):08x}{ext}"

    def _get_file_crc(self, ext):
        """获取文件CRC值"""
        # 使用脚本目录作为基准构建绝对路径
        output_target = self.script_dir.parent / self.config.chips[self.chip]['output_target']
        output_file = self._get_output_file()
        src_file = output_target / f"{output_file}{ext}"
        return self.calculate_crc32(src_file)

    def _get_output_file(self):
        """返回应该使用的输出文件名，根据boot标志选择app_output_file或boot_output_file"""
        chip_config = self.config.chips[self.chip]
        if self.boot and 'boot_output_file' in chip_config:
            return chip_config['boot_output_file']
        # Fallback to app_output_file or legacy output_file
        return chip_config.get('app_output_file', chip_config.get('output_file'))

    def _copy_to_user_output(self, src_file):
        """复制到配置指定的输出目录"""
        if not src_file.exists():  # 新增检查
            logging.error(f"源文件不存在: {src_file}")
            return

        chip_config = self.config.chips[self.chip]
        # 使用脚本目录的上级目录作为基准构建绝对路径
        output_target = self.script_dir.parent / chip_config['output_target']
        output_target.mkdir(parents=True, exist_ok=True)
        dest = output_target / src_file.name

        if dest.exists():  # 新增覆盖检查
            dest.unlink()
            logging.warning(f"已覆盖旧文件: {dest.name}")

        shutil.copy(src_file, dest)
        logging.info(f"复制到输出目录: {dest}")

    def _copy_output_files(self, output_dir):
        chip_config = self.config.chips[self.chip]
        output_file = self._get_output_file()

        for ext in ['.hex', '.bin']:
            src = Path(output_dir) / f"{output_file}{ext}"

            # 第一步：删除所有旧文件
            # 使用脚本目录的上级目录作为基准构建绝对路径
            output_target = self.script_dir.parent / chip_config['output_target']
            for old_file in output_target.glob(f"{output_file}_*{ext}"):
                try:
                    old_file.unlink()
                    logging.info(f"删除旧文件: {old_file.name}")
                except Exception as e:
                    logging.error(f"删除文件失败 {old_file.name}: {str(e)}")

            # 第二步：复制原始文件到user/output
            self._copy_to_user_output(src)

            # 第三步：生成带版本的文件并复制到目标目录
            dest_name = self._generate_filename(output_file, ext)
            dest = output_target / dest_name
            shutil.copy(src, dest)
            logging.info(f"生成带版本文件: {dest.name}")

            # 第四步：合并文件调用（仅在生成app时合并）
            if not self.boot and 'boot_output_file' in chip_config:
                output_target = self.script_dir.parent / chip_config['output_target']
                self.merge_file(output_target, ext)

    def merge_file(self, path, ext):
        current_dir = os.getcwd()
        os.chdir(path)

        if not Path("boot.hex").exists():
            logging.warning(f"未找到boot.hex文件，跳过合并操作")
            os.chdir(current_dir)
            return

        merge_exe = 'hexfile_merger.exe'
        if not self.check_exe(merge_exe):
            os.chdir(current_dir)
            return

        merged_base = "boot_add_app"
        merged_file = Path(f"{merged_base}{ext}")

        # 删除旧版本合并文件
        pattern = f"{merged_base}_v*{ext}"
        for old_file in Path('.').glob(pattern):
            try:
                old_file.unlink()
                logging.info(f"删除旧合并文件: {old_file.name}")
            except Exception as e:
                logging.error(f"删除合并文件失败 {old_file.name}: {str(e)}")

        if ext == '.hex':
            subprocess.run([merge_exe, '-v3', '-import', 'boot.hex', '-import', 'app.hex', '-export', str(merged_file)], check=True)
        elif ext == '.bin':
            os.chdir(current_dir)
            return
            # subprocess.run([merge_exe, '-v3', '-import', 'boot.bin', '0x00', '-import', 'app.bin', '0x2800', '-export', str(merged_file)], check=True)

        # 生成带版本的文件名并重命名
        versioned_name = self._generate_filename(merged_base, ext)
        versioned_path = merged_file.with_name(versioned_name)
        shutil.copy(merged_file, versioned_path)
        logging.info(f"生成合并版本文件: {versioned_path.name}")

        os.chdir(current_dir)

# MDK 构建器
class MDKBuilder(BaseBuilder):
    MDK_OUTPUT_DIR = 'app_mdk_output_dir'
    MDK_PROJECT_FILE = 'app_mdk_project_file'

    def build(self):
        self.start_animation()
        log_file_path = None
        chip_config = self.config.chips[self.chip]
        try:
            compiler_path = self.config.compilers[self.compiler]['path']
            build_type = "-r" if self.clean else "-b"
            # 判断是否为boot编译
            if self.boot and 'boot_mdk_output_dir' in chip_config and 'boot_mdk_project_file' in chip_config:
                output_dir_key = 'boot_mdk_output_dir'
                project_file_key = 'boot_mdk_project_file'
            else:
                output_dir_key = self.MDK_OUTPUT_DIR
                project_file_key = self.MDK_PROJECT_FILE
            project_dir = Path(chip_config[output_dir_key]).parent
            project_file = project_dir / Path(chip_config[project_file_key]).name
            if not project_dir.exists():
                raise FileNotFoundError(f"项目目录不存在: {project_dir}")
            if not project_file.exists():
                raise FileNotFoundError(f"项目文件不存在: {project_file}")
            log_file = chip_config.get('output_build_log', 'build_log.log')
            cmd = [
                compiler_path,
                "-j0",
                build_type,
                str(project_file),
                "-o", str(log_file)
            ]
            self._execute(cmd)
            log_file_path = Path(chip_config[output_dir_key]).parent / log_file
        except Exception as e:
            self.stop_animation_thread()
            if chip_config:
                log_file = chip_config.get('output_build_log', 'build_log.log')
                if self.boot and 'boot_mdk_output_dir' in chip_config:
                    log_file_path = Path(chip_config['boot_mdk_output_dir']).parent / log_file
                else:
                    log_file_path = Path(chip_config[self.MDK_OUTPUT_DIR]).parent / log_file
            raise
        finally:
            self.stop_animation_thread()
            if log_file_path and log_file_path.exists():
                with open(log_file_path, 'r') as f:
                    log_content = f.read()
                    logging.info(f"MDK build log:\n{log_content}")
        if chip_config:
            if self.boot and 'boot_mdk_output_dir' in chip_config:
                self._copy_output_files(chip_config['boot_mdk_output_dir'])
            else:
                self._copy_output_files(chip_config[self.MDK_OUTPUT_DIR])

    def _execute(self, cmd):
        logging.info(f"Executing: {' '.join(cmd)}")
        result = subprocess.run(cmd)
        if result.returncode != 0:
            # 从build_config.json的output_build_log字段获取日志文件名
            chip_config = self.config.chips[self.chip]
            log_file_name = chip_config["output_build_log"]  # 动态获取日志文件名
            log_file_path = Path(chip_config[self.MDK_OUTPUT_DIR]).parent / log_file_name
            if log_file_path.exists():
                with open(log_file_path, 'r') as f:
                    log_content = f.read()
                    if "Error(s)" in log_content and not "0 Error(s)" in log_content:
                        raise RuntimeError(f"MDK编译失败（含实际错误），返回码：{result.returncode}")
                    else:
                        logging.warning(f"MDK编译完成（仅警告），返回码：{result.returncode}")
            else:
                raise RuntimeError(f"MDK编译失败，返回码：{result.returncode}，日志文件 {log_file_path} 不存在")

    def _copy_output_files(self, output_dir):
        super()._copy_output_files(output_dir)

class IARBuilder(BaseBuilder):
    def build(self):
        self.start_animation()
        log_path = None
        chip_config = self.config.chips[self.chip]
        try:
            compiler_path = Path(self.config.compilers[self.compiler]['path'])
            # 判断是否为boot编译
            if self.boot and 'boot_iar_project_file' in chip_config and 'boot_iar_output_dir' in chip_config:
                project_file = Path(chip_config['boot_iar_project_file'])
                output_dir = Path(chip_config['boot_iar_output_dir'])
                config_name = chip_config.get('boot_iar_config', 'boot')
            else:
                project_file = Path(chip_config['app_iar_project_file'])
                output_dir = Path(chip_config['app_iar_output_dir'])
                config_name = chip_config.get('app_iar_config')
            log_path = project_file.parent / "build_log.log"
            if not project_file.parent.exists():
                raise FileNotFoundError(f"项目目录不存在: {project_file.parent}")
            if not project_file.exists():
                raise FileNotFoundError(f"项目文件不存在: {project_file}")
            build_action = "-build" if self.clean else "-make"
            build_cmd = [
                str(compiler_path),
                str(project_file),
                build_action,
                config_name,
                '-parallel', '8',
                '-log', 'info'
            ]
            try:
                process = subprocess.run(
                    build_cmd,
                    check=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    universal_newlines=True
                )
                with open(log_path, 'w', encoding='utf-8') as f:
                    f.write(process.stdout)
                    if process.stderr:
                        f.write("\nErrors:\n")
                        f.write(process.stderr)
            except subprocess.CalledProcessError as e:
                if log_path.exists():
                    with open(log_path, 'w', encoding='utf-8') as f:
                        f.write(e.stdout)
                        if e.stderr:
                            f.write("\nErrors:\n")
                            f.write(e.stderr)
                raise RuntimeError(f"IAR构建失败\n{str(e)}") from e
            output_file = self._get_output_file()
            elf_file = output_dir / f"{output_file}.out"
            bin_file = output_dir / f"{output_file}.bin"
            ielftool_cmd = [
                "ielftool",
                "--bin",
                str(elf_file),
                str(bin_file)
            ]
            subprocess.run(ielftool_cmd, check=True)
        except Exception as e:
            self.stop_animation_thread()
            if log_path and log_path.exists():
                with open(log_path) as f:
                    log_content = f.read()
                    logging.info(f"IAR build log:\n{log_content}")
            raise e
        finally:
            self.stop_animation_thread()
            if log_path and log_path.exists():
                with open(log_path) as f:
                    log_content = f.read()
                    logging.info(f"IAR build log:\n{log_content}")
        if self.boot and 'boot_iar_output_dir' in chip_config:
            self._copy_output_files(chip_config['boot_iar_output_dir'])
        else:
            self._copy_output_files(chip_config['app_iar_output_dir'])

    def _execute_with_shell(self, cmd):
        """处理Windows路径空格问题"""
        logging.info(f"Executing: {cmd}")
        try:
            # 将命令字符串分割成命令列表
            if isinstance(cmd, str):
                import shlex
                cmd = shlex.split(cmd)

            subprocess.run(
                cmd,
                check=True,
                stderr=subprocess.PIPE,
                universal_newlines=True
            )
        except subprocess.CalledProcessError as e:
            error_msg = f"Command failed with code {e.returncode}\nError output:\n{e.stderr}"
            raise RuntimeError(error_msg) from e

    def _copy_output_files(self, output_dir):
        super()._copy_output_files(output_dir)

# GCC 构建器
class GCCBuilder(BaseBuilder):
    def build(self):
        chip_config = self.config.chips[self.chip]
        original_cwd = os.getcwd()
        try:
            # 判断是否为boot编译
            if self.boot and 'boot_cmake_build_dir' in chip_config and 'boot_cmake_source_dir' in chip_config:
                build_dir = Path(chip_config['boot_cmake_build_dir']).resolve()
                source_dir = Path(chip_config['boot_cmake_source_dir']).resolve()
            else:
                build_dir = Path(chip_config['app_cmake_build_dir']).resolve()
                source_dir = build_dir.parent
            if not source_dir.exists():
                raise FileNotFoundError(f"源代码目录不存在: {source_dir}")
            cmake_file = source_dir / "CMakeLists.txt"
            if not cmake_file.exists():
                raise FileNotFoundError(f"CMakeLists.txt文件不存在: {cmake_file}")
            if self.clean and build_dir.exists():
                shutil.rmtree(build_dir)
                logging.info("已执行全量编译清理")
            build_dir.mkdir(parents=True, exist_ok=True)
            logging.info(f"构建目录绝对路径: {build_dir}")
            os.chdir(build_dir)
            self._execute([
                "cmake",
                "-G", "Unix Makefiles",
                ".."
            ])
            if self.clean:
                self._execute(["make", "clean"])
                logging.info("执行全量编译")
            else:
                logging.info("执行增量编译")
            self._execute(["make", "-j8"])
            os.chdir(original_cwd)
            self._copy_output_files(build_dir)
        except Exception as e:
            os.chdir(original_cwd)
            raise e

    def _execute(self, cmd):
        """从BaseBuilder继承执行方法"""
        logging.info(f"Executing: {' '.join(cmd)}")
        subprocess.run(cmd, check=True)

    def _copy_output_files(self, output_dir):
        """覆盖基类方法，确保路径都是绝对路径"""
        chip_config = self.config.chips[self.chip]
        # 确保output_dir是绝对路径
        output_dir = Path(output_dir).resolve()

        # 使用正确的输出文件名
        output_file = self._get_output_file()

        for ext in ['.hex', '.bin']:
            src = output_dir / f"{output_file}{ext}"

            # 检查文件是否存在
            if not src.exists():
                logging.error(f"GCC构建产物文件不存在: {src}")
                continue

            # 第一步：删除所有旧文件
            # 使用脚本目录作为基准构建绝对路径
            output_target = self.script_dir.parent / chip_config['output_target']
            output_target.mkdir(parents=True, exist_ok=True)

            for old_file in output_target.glob(f"{output_file}_*{ext}"):
                try:
                    old_file.unlink()
                    logging.info(f"删除旧文件: {old_file.name}")
                except Exception as e:
                    logging.error(f"删除文件失败 {old_file.name}: {str(e)}")

            # 第二步：复制原始文件到user/output
            dest = output_target / src.name
            shutil.copy(src, dest)
            logging.info(f"复制到输出目录: {dest}")

            # 第三步：生成带版本的文件并复制到目标目录
            dest_name = self._generate_filename(output_file, ext)
            versioned_dest = output_target / dest_name
            shutil.copy(src, versioned_dest)
            logging.info(f"生成带版本文件: {versioned_dest}")

# 编程器基类
class BaseProgrammer:
    def __init__(self, chip: str, programmer: str, boot: bool = False, app: bool = False, boot_add_app: bool = False):
        self.config = ConfigManager()
        self.chip = chip
        self.programmer = programmer
        self.boot = boot
        self.app = app
        self.boot_add_app = boot_add_app
        self.validate_config()

    @classmethod
    def _execute(self, cmd):
        logging.info(f"Executing: {' '.join(cmd)}")
        subprocess.run(cmd, check=True)

    def validate_config(self):
        if self.programmer not in self.config.programmers:
            available = list(self.config.programmers.keys())
            raise ValueError(f"不支持的编程器: {self.programmer}，可用选项: {available}")

# J-Link 编程器
class JLinkProgrammer(BaseProgrammer):
    def program(self):
        tool_path = self.config.programmers[self.programmer]['path']

        # 根据参数决定烧录脚本
        if self.boot_add_app:
            script = self._generate_script(mode="combined_file")
        elif self.boot and self.app:
            script = self._generate_script(mode="boot_add_app")
        elif self.boot:
            script = self._generate_script(mode="boot")
        elif self.app:
            script = self._generate_script(mode="app")
        else:
            # 默认情况，保持向后兼容性
            script = self._generate_script(mode="app")

        cmd = [
            tool_path,
            "-device", self.config.chips[self.chip]['family'],
            "-if", "SWD",
            "-speed", "4000",
            "-CommanderScript", script
        ]
        self._execute(cmd)

    def _generate_script(self, mode="app"):
        chip_config = self.config.chips[self.chip]
        # 使用绝对路径
        script_dir = Path(__file__).parent.parent.absolute()
        output_path = script_dir / chip_config['output_target']

        if mode == "combined_file":
            # 直接使用合并后的boot_add_app文件
            logging.info("准备烧录合并固件boot_add_app")
            try:
                # 查找最新的boot_add_app合并文件
                combined_files = list(output_path.glob("boot_add_app_v*_*.hex"))
                if not combined_files:
                    raise FileNotFoundError(f"未找到合并固件 boot_add_app .hex 文件，路径: {output_path}")

                latest_combined_file = max(combined_files, key=lambda p: p.stat().st_mtime)
                load_path = (output_path / latest_combined_file.name).as_posix()

                logging.info(f"烧录合并固件文件: {load_path}")

                script = f"""
                si 1
                speed 10000
                device {chip_config['family']}
                r
                h
                erase
                loadfile {load_path}
                r
                ms 10
                q
                """

                script_name = f"{chip_config['family']}_combined.jlink"
                script_path = output_path / script_name

                # 检查脚本文件是否存在并在生成新脚本前删除
                if script_path.exists():
                    try:
                        script_path.unlink()
                        logging.info(f"已删除旧的 J-Link 脚本: {script_path}")
                    except OSError as e:
                        logging.error(f"删除旧 J-Link 脚本失败 {script_path}: {e}")

                # 创建目录并写入脚本
                script_path.parent.mkdir(parents=True, exist_ok=True)
                with open(script_path, 'w') as f:
                    f.write(script)
                logging.info(f"已生成 J-Link 脚本: {script_path}")
                return str(script_path)

            except Exception as e:
                logging.error(f"生成合并固件烧录脚本时出错: {e}")
                raise e
        elif mode == "boot":
            base_filename = chip_config.get('boot_output_file', chip_config.get('output_file'))
            logging.info(f"准备烧录Bootloader: {base_filename}")
        elif mode == "app":
            base_filename = chip_config.get('app_output_file', chip_config.get('output_file'))
            logging.info(f"准备烧录应用程序: {base_filename}")
        elif mode == "boot_add_app":
            # 先找到boot和app的最新文件
            boot_filename = chip_config.get('boot_output_file', None)
            app_filename = chip_config.get('app_output_file', chip_config.get('output_file'))
            logging.info(f"准备烧录Bootloader和应用程序: {boot_filename} + {app_filename}")

            if not boot_filename:
                raise ValueError(f"芯片配置中未找到bootloader配置: {self.chip}")

            # 以下代码专门处理bootapp模式
            try:
                # 获取bootloader和app的最新文件
                boot_hex_files = list(output_path.glob(f"{boot_filename}_v*_*.hex"))
                app_hex_files = list(output_path.glob(f"{app_filename}_v*_*.hex"))

                if not boot_hex_files:
                    raise FileNotFoundError(f"未找到Bootloader .hex文件，路径: {output_path}，文件名前缀: {boot_filename}")
                if not app_hex_files:
                    raise FileNotFoundError(f"未找到应用程序 .hex文件，路径: {output_path}，文件名前缀: {app_filename}")

                latest_boot_hex = max(boot_hex_files, key=lambda p: p.stat().st_mtime)
                latest_app_hex = max(app_hex_files, key=lambda p: p.stat().st_mtime)

                boot_load_path = (output_path / latest_boot_hex.name).as_posix()
                app_load_path = (output_path / latest_app_hex.name).as_posix()

                logging.info(f"烧录Bootloader文件: {boot_load_path}")
                logging.info(f"烧录应用程序文件: {app_load_path}")

                # 生成包含两个烧录命令的脚本
                script = f"""
                si 1
                speed 10000
                device {chip_config['family']}
                r
                h
                erase
                loadfile {boot_load_path}
                loadfile {app_load_path}
                r
                ms 10
                q
                """

                script_name = f"{chip_config['family']}_bootapp.jlink"
                script_path = output_path / script_name

                # 检查脚本文件是否存在并在生成新脚本前删除
                if script_path.exists():
                    try:
                        script_path.unlink()
                        logging.info(f"已删除旧的 J-Link 脚本: {script_path}")
                    except OSError as e:
                        logging.error(f"删除旧 J-Link 脚本失败 {script_path}: {e}")

                # 创建目录并写入脚本
                script_path.parent.mkdir(parents=True, exist_ok=True)
                with open(script_path, 'w') as f:
                    f.write(script)
                logging.info(f"已生成 J-Link 脚本: {script_path}")
                return str(script_path)

            except Exception as e:
                logging.error(f"生成boot+app烧录脚本时出错: {e}")
                raise e

        # 以下是单文件烧录模式处理(boot或app)
        try:
            versioned_files = list(output_path.glob(f"{base_filename}_v*_*.hex"))
            if not versioned_files:
                raise FileNotFoundError(f"No versioned .hex files found in {output_path} for {base_filename}")
            latest_hex_file = max(versioned_files, key=lambda p: p.stat().st_mtime)
            # 使用绝对路径
            absolute_load_path = (output_path / latest_hex_file.name).as_posix()
            logging.info(f"使用最新的固件文件进行烧录: {absolute_load_path}")
        except FileNotFoundError as e:
            logging.error(e)
            raise e
        except Exception as e:
            logging.error(f"查找最新固件文件时出错: {e}")
            raise e

        script = f"""
        si 1
        speed 10000
        device {chip_config['family']}
        r
        h
        erase
        loadfile {absolute_load_path}
        r
        ms 10
        q
        """

        mode_suffix = "boot" if mode == "boot" else "app"
        script_name = f"{chip_config['family']}_{mode_suffix}.jlink"
        script_path = output_path / script_name

        # Check if the script file exists and remove it before generating a new one
        if script_path.exists():
            try:
                script_path.unlink()
                logging.info(f"已删除旧的 J-Link 脚本: {script_path}")
            except OSError as e:
                logging.error(f"删除旧 J-Link 脚本失败 {script_path}: {e}")

        # 创建目录并写入脚本
        script_path.parent.mkdir(parents=True, exist_ok=True)
        with open(script_path, 'w') as f:
            f.write(script)
        logging.info(f"已生成 J-Link 脚本: {script_path}")
        return str(script_path)

# daplink 编程器
class DapLinkProgrammer(BaseProgrammer):
    def program(self):
        programmer_config = self.config.programmers.get(self.programmer, {})
        tool_choice = programmer_config.get('tool', 'pyocd') # 默认

        if tool_choice == 'pyocd':
            self._program_pyocd(programmer_config)
        else: # 默认为 openocd
            self._program_openocd(programmer_config)

    def _program_openocd(self, config):
        tool_path = config['path']
        config_file = self._generate_openocd_config()
        cmd = [tool_path, "-f", config_file]
        self._execute(cmd)

    def _generate_openocd_config(self):
        hex_file = self._find_latest_hex_file()
        cfg = f"""
        source [find interface/{self.programmer}.cfg]
        transport select swd
        source [find target/{self.config.chips[self.chip]['family']}.cfg]
        program {{{hex_file}}} verify
        reset
        shutdown
        """
        config_path = Path('openocd.cfg')
        config_path.write_text(cfg)
        logging.info(f"Generated openocd config: {config_path.resolve()}")
        return str(config_path)

    def _program_pyocd(self, config):
        tool_path = config.get('pyocd_path', 'pyocd') # 从配置或默认值获取
        hex_file = self._find_latest_hex_file()
        cmd = [
            tool_path,
            "flash",
            "-t", self.config.chips[self.chip]['family'],
            str(hex_file)
        ]

        # Add frequency to control clock speed, which can help with connection issues
        frequency = config.get('frequency')
        if frequency:
            cmd.extend(['--frequency', str(frequency)])

        self._execute(cmd)

    def _find_latest_hex_file(self):
        chip_config = self.config.chips[self.chip]
        output_path = Path(__file__).parent.absolute() / chip_config['output_target']

        if self.boot_add_app:
            base_filename = "boot_add_app"
        elif self.boot:
            base_filename = chip_config.get('boot_output_file', 'boot')
        else: # 默认 app
            base_filename = chip_config.get('app_output_file', 'app')

        logging.info(f"Searching for hex files with base name: {base_filename} in {output_path}")

        versioned_files = list(output_path.glob(f"{base_filename}_v*_*.hex"))
        if not versioned_files:
             # 如果没有版本文件，尝试查找原始文件
            original_file = output_path / f"{base_filename}.hex"
            if original_file.exists():
                logging.warning(f"No versioned hex file found for {base_filename}, using non-versioned file: {original_file}")
                return original_file.resolve()
            raise FileNotFoundError(f"No versioned or non-versioned .hex file found in {output_path} for {base_filename}")

        latest_hex_file = max(versioned_files, key=lambda p: p.stat().st_mtime)
        logging.info(f"Found latest hex file: {latest_hex_file}")
        return latest_hex_file.resolve()

class StLinkProgrammer(BaseProgrammer):
    def program(self):
        tool_path = self.config.programmers[self.programmer]['path']
        self._generate_script()

    def _generate_script(self):
        chip_config = self.config.chips[self.chip]
        output_path = Path(chip_config['output_target'])
        base_filename = chip_config['app_output_file']

# 命令行处理
def parse_args():
    CHIPS = '-c'
    HELP = '?'
    TOOLS = '-t'
    PROGRAMMERS = '-p'

    # 芯片列表查询功能
    if len(sys.argv) == 3 and CHIPS in sys.argv and HELP in sys.argv:
        print("\n支持的芯片型号:")
        for chip in ConfigManager.get_chip_list():
            print(f"  • {chip}")
        sys.exit(0)

    parser = argparse.ArgumentParser(
        description=ConfigManager.generate_usage(),
        formatter_class=ChipHelpFormatter
    )

    subparsers = parser.add_subparsers(dest='command', required=True)

    # Build命令解析器
    build_parser = subparsers.add_parser('build', help='Build firmware')
    build_parser.add_argument(CHIPS, '--chip', required=True,
                            choices=ConfigManager.get_chip_list(),
                            metavar='CHIPS',
                            help='目标芯片型号 (使用 -c ? 查看列表)')
    build_parser.add_argument(TOOLS, '--tool', required=True,  # 新增工具链参数
                            choices=ConfigManager.BUILD_TOOLS,
                            help='编译工具链')
    build_parser.add_argument('--clean', action='store_true',  # 新增clean参数
                            help='执行全量编译')
    build_parser.add_argument('--boot', action='store_true',  # 新增boot参数
                            help='编译bootloader')

    # Program命令解析器
    program_parser = subparsers.add_parser('program', help='Program device')
    program_parser.add_argument(PROGRAMMERS, '--programmer', required=True,
                              choices=ConfigManager.DOWNLOAD_TOOLS,
                              metavar='PROGRAMMER',  # 新增metavar
                              help='编程器类型[j-link, dap-link, st-link]')
    program_parser.add_argument(CHIPS, '--chip', required=True,
                              choices=ConfigManager.get_chip_list(),
                              metavar='CHIPS',  # 统一metavar
                              help='目标芯片型号 (使用 -c ? 查看列表)')
    program_parser.add_argument('--boot', action='store_true',
                              help='烧录bootloader')
    program_parser.add_argument('--app', action='store_true',
                              help='烧录应用程序')
    program_parser.add_argument('--boot_add_app', action='store_true',
                              help='烧录引导和应用程序')

    return parser.parse_args()

# 构建工厂
class BuilderFactory:
    @staticmethod
    def create(args):
        if args.command == 'build':
            return {
                tool: globals()[f"{tool.upper()}Builder"]
                for tool in ConfigManager.BUILD_TOOLS
            }[args.tool](args.chip, args.tool, args.clean, getattr(args, 'boot', False))
        elif args.command == 'program':
            return {
                tool: globals()[f"{tool.replace('-', ' ').title().replace(' ', '')}Programmer"]
                for tool in ConfigManager.DOWNLOAD_TOOLS
            }[args.programmer](
                args.chip,
                args.programmer,
                getattr(args, 'boot', False),
                getattr(args, 'app', False),
                getattr(args, 'boot_add_app', False)
            )
        else:
            raise ValueError(f"无效命令: {args.command}")

# 主函数
def main():
    args = parse_args()
    if args is None:
        return

    # 初始化日志
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('build.log', mode='w', encoding='utf-8'),  # 添加编码参数
            logging.StreamHandler()
        ]
    )

    try:
        executor = BuilderFactory.create(args)
        if args.command == 'build':
            executor.build()
        elif args.command == 'program':
            # 检查program命令的参数
            if args.command == 'program' and not (args.boot or args.app or args.boot_add_app):
                # 如果没有指定boot、app或boot_add_app，默认设置app为True
                args.app = True
                logging.info("未指定烧录模式，默认烧录应用程序")
            executor.program()
        logging.info("Operation completed successfully")
    except Exception as e:
        logging.error(f"Operation failed: {str(e)}")
        sys.exit(1)

if __name__ == "__main__":
    start = datetime.now(tz = timezone.utc)
    main()
    end = datetime.now(tz = timezone.utc)
    logging.info('run time: %s second', (end - start))